﻿
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Timers;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;


namespace Heartbeat
{

    public delegate void MessageCallbackType(TcpClient pClient, Packet pPacket);
    public delegate void ChannelsCallbackType(TcpClient pClient, List<String> channels);
    public delegate void ConnectCallbackType(TcpClient pClient);
    public delegate void DisconnectCallbackType(TcpClient pClient);

    public class TcpClient
    {
        protected TcpConnection mConnection;
        protected bool mChannelsReceived = false;
        protected bool mConnected = false;
        protected Timer mTimer;
        protected bool mPingReceived = true;
        protected String mVersion = "1.0.0";
        protected MessageCallbackType mOnMessageCallback = null;
        protected MessageCallbackType mOnAuthenticationCallback = null;
        protected ChannelsCallbackType mOnChannelsReceivedCallback = null;
        protected ConnectCallbackType mOnConnectCallback = null;
        protected DisconnectCallbackType mOnDisconnectCallback = null;
        protected Dictionary<String, int> mChannelMap = null;
        protected Queue<KeyValuePair<String, JObject>> mWriteChannelMessagePairs = null;
        protected Queue<Packet> mWritePackets = null;
        enum Channels
        {
            LIBRARY_VERSION = 1,
            LIBRARY_CHANNEL_LIST,
            LIBRARY_CHANNEL_SUBSCRIPTION,
            LIBRARY_CHANNEL_PING_PONG,
            LIBRARY_AUTHENTICATION,
            PREDEFINED_MAX
        };

        public TcpClient(bool useSsl)
        {
            mConnection = new TcpConnection(useSsl);

            mConnection.SetInternalOnMessageCallback(new InternalMessageCallbackType(MessageReceived));
            mConnection.SetInternalOnReadyCallback(new InternalReadyCallbackType(OnReady));
            mChannelMap = new Dictionary<String, int>();
            mWriteChannelMessagePairs = new Queue<KeyValuePair<String, JObject>>();
            mWritePackets = new Queue<Packet>();
            mTimer = new Timer();
        }

        public virtual void Close()
        {
            mConnected = false;
            mConnection.Close();
            if(this.mOnDisconnectCallback != null)
            {
               this.mOnDisconnectCallback(this);
            }
            this.mTimer.Close();
        }

        public string GetChannelName(int channelNumber)
        {
            foreach (KeyValuePair<string, int> item in mChannelMap)
            {
                if (item.Value == channelNumber)
                    return item.Key;
            }

            return null;
        }

        protected void SendPing(object sender, ElapsedEventArgs e)
        {
           if (!this.mPingReceived)
           {
              this.Close();
           }
           else
           {
              this.mPingReceived = false;
              JObject pMessage = new JObject();
              pMessage["p"] = "p";
              this.Write(new Packet(4, pMessage));
           }
        }

        public void SetOnAuthenticationCallback(MessageCallbackType cb)
        {
            mOnAuthenticationCallback = cb;
        }

        public void SetOnChannelsReceivedCallback(ChannelsCallbackType cb)
        {
            mOnChannelsReceivedCallback = cb;
        }

        public void SetOnConnectCallback(ConnectCallbackType cb)
        {
            mOnConnectCallback = cb;
        }

        public void SetOnDisconnectCallback(DisconnectCallbackType cb)
        {
           this.mOnDisconnectCallback = cb;
        }

        public void SetOnMessageCallback(MessageCallbackType cb)
        {
            mOnMessageCallback = cb;
        }

        public virtual void Start(String address, int port)
        {
            mConnection.Start(address, port);
        }

        public virtual void SendAuthentication(JObject authData)
        {
            mConnection.Write(new Packet((int)Channels.LIBRARY_AUTHENTICATION, authData));
        }

        public void SubscribeTo(String channel)
        {
            SubscribeTo(new List<String> { channel });
        }

        public void SubscribeTo(IList<String> channels)
        {
            JObject outMessage = new JObject();
            JArray channelListJson = new JArray();
            foreach (String cName in channels)
            {
                if (!mChannelMap.ContainsKey(cName))
                    continue;

                int channelNumber = mChannelMap[cName];
                channelListJson.Add(channelNumber);
            }

            outMessage["channels"] = channelListJson;
            mConnection.Write(new Packet((int)Channels.LIBRARY_CHANNEL_SUBSCRIPTION, outMessage));
        }

        protected virtual void OnReady()
        {
            mConnected = true;

            //Send the queued packets.
            while (mWritePackets.Count > 0)
            {
                mConnection.Write(mWritePackets.Dequeue());
            }

            if (mOnConnectCallback == null)
                return;

            mOnConnectCallback(this);
        }

        public virtual void Write(Packet pPacket)
        {
            if (!mConnected)
            {
                mWritePackets.Enqueue(pPacket);
                return;
            }

            mConnection.Write(pPacket);
        }

        public virtual void Write(String channelName, JObject msgJson)
        {
            if (!mChannelsReceived)
            {
                mWriteChannelMessagePairs.Enqueue(new KeyValuePair<String, JObject>(channelName, msgJson));
                return;
            }

            if (!mChannelMap.ContainsKey(channelName))
                return;

            Write(new Packet(mChannelMap[channelName], msgJson));
        }

        protected void HandleChannelListReceived(JObject msgJson)
        {
            mChannelMap.Clear();

            JArray channelList = (JArray)msgJson["channels"];

            for (int i = 0; i < channelList.Count; ++i)
            {
                JObject channel = (JObject)channelList[i];
                //IList<string> names = channel.Properties().Select(p => p.Name).ToList();
                IEnumerable<JProperty> properties = channel.Properties();
                IList<string> names = new List<string>();
                foreach(JProperty property in properties)
                    names.Add(property.Name);

                if (names.Count == 0)
                    continue;

                string cName = names[0];
                int cNumber = (int)channel[cName];
                mChannelMap.Add(cName, cNumber);
            }

            if (mOnChannelsReceivedCallback == null)
                return;

            List<string> channelNames = new List<string>();
            foreach (KeyValuePair<String, int> entry in mChannelMap)
            {
                channelNames.Add(entry.Key);
            }

            mChannelsReceived = true;

            //Empty the message queue.
            while (mWriteChannelMessagePairs.Count > 0)
            {
                KeyValuePair<String, JObject> front = mWriteChannelMessagePairs.Dequeue();
                Write(front.Key, front.Value);
            }

            mOnChannelsReceivedCallback(this, channelNames);
        }

        protected virtual void MessageReceived(string message, int channel)
        {
            JObject msgJson = JObject.Parse(message);
            Packet pPacket = new Packet(channel, msgJson);

            MessageReceived(pPacket);
        }

        protected virtual void MessageReceived(Packet pPacket)
        {

            int channel = pPacket.GetChannel();
            JObject msgJson = pPacket.GetMessage();

            if (channel == (int)Channels.LIBRARY_VERSION)
            {
                if (!VersionCheck(msgJson))
                {
                    Console.WriteLine("Version check failed");
                    Close();
                    return;
                }

                return;
            }

            if (channel == (int)Channels.LIBRARY_CHANNEL_LIST)
            {
                HandleChannelListReceived(msgJson);
                return;
            }

            if (channel == (int)Channels.LIBRARY_AUTHENTICATION)
            {
                if (mOnAuthenticationCallback != null)
                    mOnAuthenticationCallback(this, pPacket);
                this.mTimer = new Timer();
                this.mTimer.Elapsed += new ElapsedEventHandler(this.SendPing);
                this.mTimer.Interval = 5000.0f;
                this.mTimer.Enabled = true;
            }

            if (channel == (int)Channels.LIBRARY_CHANNEL_PING_PONG)
            {
               this.mPingReceived = true;
            }

            if (channel <= (int)Channels.PREDEFINED_MAX)
                return;

            if (mOnMessageCallback == null)
                return;


            mOnMessageCallback(this, pPacket);
        }

        protected virtual bool VersionCheck(JObject msgJson)
        {
            try
            {
                String serverVersion = (String)msgJson["version"];
                char[] delimiters = { '.' };
                String[] serverxyz = serverVersion.Split(delimiters);
                String[] clientxyz = mVersion.Split(delimiters);
                if (serverxyz.Length != 3 || clientxyz.Length != 3 || !serverxyz[0].Equals(clientxyz[0]) || !serverxyz[1].Equals(clientxyz[1]))
                    return false;

                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                return false;
            }
        }
    }
}
